1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 package com.sun.security.sasl.digest;
27
28 import java.security.AccessController;
29 import java.security.MessageDigest;
30 import java.security.NoSuchAlgorithmException;
31 import java.io.ByteArrayOutputStream;
32 import java.io.ByteArrayInputStream;
33 import java.io.IOException;
34 import java.io.UnsupportedEncodingException;
35 import java.util.StringTokenizer;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Set;
40 import java.util.Arrays;
41
42 import java.util.logging.Logger;
43 import java.util.logging.Level;
44
45 import javax.security.sasl.*;
46 import javax.security.auth.callback.CallbackHandler;
47 import javax.security.auth.callback.PasswordCallback;
48 import javax.security.auth.callback.NameCallback;
49 import javax.security.auth.callback.Callback;
50 import javax.security.auth.callback.UnsupportedCallbackException;
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103 final class DigestMD5Client extends DigestMD5Base implements SaslClient {
104 private static final String MY_CLASS_NAME = DigestMD5Client.class.getName();
105
106
107 private static final String CIPHER_PROPERTY =
108 "com.sun.security.sasl.digest.cipher";
109
110
111 private static final String[] DIRECTIVE_KEY = {
112 "realm",
113 "qop",
114 "algorithm",
115 "nonce",
116 "maxbuf",
117 "charset",
118 "cipher",
119 "rspauth",
120 "stale",
121 };
122
123
124 private static final int REALM = 0;
125 private static final int QOP = 1;
126 private static final int ALGORITHM = 2;
127 private static final int NONCE = 3;
128 private static final int MAXBUF = 4;
129 private static final int CHARSET = 5;
130 private static final int CIPHER = 6;
131 private static final int RESPONSE_AUTH = 7;
132 private static final int STALE = 8;
133
134 private int nonceCount;
135
136
137 private String specifiedCipher;
138 private byte[] cnonce;
139 private String username;
140 private char[] passwd;
141 private byte[] authzidBytes;
142
143
144
145
146
147
148
149
150
151
152
153
154
155 DigestMD5Client(String authzid, String protocol, String serverName,
156 Map props, CallbackHandler cbh) throws SaslException {
157
158 super(props, MY_CLASS_NAME, 2, protocol + "/" + serverName, cbh);
159
160
161 if (authzid != null) {
162 this.authzid = authzid;
163 try {
164 authzidBytes = authzid.getBytes("UTF8");
165
166 } catch (UnsupportedEncodingException e) {
167 throw new SaslException(
168 "DIGEST-MD5: Error encoding authzid value into UTF-8", e);
169 }
170 }
171
172 if (props != null) {
173 specifiedCipher = (String)props.get(CIPHER_PROPERTY);
174
175 logger.log(Level.FINE, "DIGEST60:Explicitly specified cipher: {0}",
176 specifiedCipher);
177 }
178 }
179
180
181
182
183
184
185 public boolean hasInitialResponse() {
186 return false;
187 }
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204 public byte[] evaluateChallenge(byte[] challengeData) throws SaslException {
205
206 if (challengeData.length > MAX_CHALLENGE_LENGTH) {
207 throw new SaslException(
208 "DIGEST-MD5: Invalid digest-challenge length. Got: " +
209 challengeData.length + " Expected < " + MAX_CHALLENGE_LENGTH);
210 }
211
212
213 byte[][] challengeVal;
214
215 switch (step) {
216 case 2:
217
218
219
220 List<byte[]> realmChoices = new ArrayList<byte[]>(3);
221 challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
222 realmChoices, REALM);
223
224 try {
225 processChallenge(challengeVal, realmChoices);
226 checkQopSupport(challengeVal[QOP], challengeVal[CIPHER]);
227 ++step;
228 return generateClientResponse(challengeVal[CHARSET]);
229 } catch (SaslException e) {
230 step = 0;
231 clearPassword();
232 throw e;
233 } catch (IOException e) {
234 step = 0;
235 clearPassword();
236 throw new SaslException("DIGEST-MD5: Error generating " +
237 "digest response-value", e);
238 }
239
240 case 3:
241 try {
242
243
244 challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
245 null, REALM);
246 validateResponseValue(challengeVal[RESPONSE_AUTH]);
247
248
249
250 if (integrity && privacy) {
251 secCtx = new DigestPrivacy(true );
252 } else if (integrity) {
253 secCtx = new DigestIntegrity(true );
254 }
255
256 return null;
257 } finally {
258 clearPassword();
259 step = 0;
260 completed = true;
261 }
262
263 default:
264
265 throw new SaslException("DIGEST-MD5: Client at illegal state");
266 }
267 }
268
269
270
271
272
273
274
275
276
277
278
279 private void processChallenge(byte[][] challengeVal, List<byte[]> realmChoices)
280 throws SaslException, UnsupportedEncodingException {
281
282
283 if (challengeVal[CHARSET] != null) {
284 if (!"utf-8".equals(new String(challengeVal[CHARSET], encoding))) {
285 throw new SaslException("DIGEST-MD5: digest-challenge format " +
286 "violation. Unrecognised charset value: " +
287 new String(challengeVal[CHARSET]));
288 } else {
289 encoding = "UTF8";
290 useUTF8 = true;
291 }
292 }
293
294
295 if (challengeVal[ALGORITHM] == null) {
296 throw new SaslException("DIGEST-MD5: Digest-challenge format " +
297 "violation: algorithm directive missing");
298 } else if (!"md5-sess".equals(new String(challengeVal[ALGORITHM], encoding))) {
299 throw new SaslException("DIGEST-MD5: Digest-challenge format " +
300 "violation. Invalid value for 'algorithm' directive: " +
301 challengeVal[ALGORITHM]);
302 }
303
304
305 if (challengeVal[NONCE] == null) {
306 throw new SaslException("DIGEST-MD5: Digest-challenge format " +
307 "violation: nonce directive missing");
308 } else {
309 nonce = challengeVal[NONCE];
310 }
311
312 try {
313
314 String[] realmTokens = null;
315
316 if (challengeVal[REALM] != null) {
317 if (realmChoices == null || realmChoices.size() <= 1) {
318
319 negotiatedRealm = new String(challengeVal[REALM], encoding);
320 } else {
321 realmTokens = new String[realmChoices.size()];
322 for (int i = 0; i < realmTokens.length; i++) {
323 realmTokens[i] =
324 new String(realmChoices.get(i), encoding);
325 }
326 }
327 }
328
329 NameCallback ncb = authzid == null ?
330 new NameCallback("DIGEST-MD5 authentication ID: ") :
331 new NameCallback("DIGEST-MD5 authentication ID: ", authzid);
332 PasswordCallback pcb =
333 new PasswordCallback("DIGEST-MD5 password: ", false);
334
335 if (realmTokens == null) {
336
337
338 RealmCallback tcb =
339 (negotiatedRealm == null? new RealmCallback("DIGEST-MD5 realm: ") :
340 new RealmCallback("DIGEST-MD5 realm: ", negotiatedRealm));
341
342 cbh.handle(new Callback[] {tcb, ncb, pcb});
343
344
345 negotiatedRealm = tcb.getText();
346 if (negotiatedRealm == null) {
347 negotiatedRealm = "";
348 }
349 } else {
350 RealmChoiceCallback ccb = new RealmChoiceCallback(
351 "DIGEST-MD5 realm: ",
352 realmTokens,
353 0, false);
354 cbh.handle(new Callback[] {ccb, ncb, pcb});
355
356
357 negotiatedRealm = realmTokens[ccb.getSelectedIndexes()[0]];
358 }
359
360 passwd = pcb.getPassword();
361 pcb.clearPassword();
362 username = ncb.getName();
363
364 } catch (UnsupportedCallbackException e) {
365 throw new SaslException("DIGEST-MD5: Cannot perform callback to " +
366 "acquire realm, authentication ID or password", e);
367
368 } catch (IOException e) {
369 throw new SaslException(
370 "DIGEST-MD5: Error acquiring realm, authentication ID or password", e);
371 }
372
373 if (username == null || passwd == null) {
374 throw new SaslException(
375 "DIGEST-MD5: authentication ID and password must be specified");
376 }
377
378
379 int srvMaxBufSize =
380 (challengeVal[MAXBUF] == null) ? DEFAULT_MAXBUF
381 : Integer.parseInt(new String(challengeVal[MAXBUF], encoding));
382 sendMaxBufSize =
383 (sendMaxBufSize == 0) ? srvMaxBufSize
384 : Math.min(sendMaxBufSize, srvMaxBufSize);
385 }
386
387
388
389
390
391
392
393
394 private void checkQopSupport(byte[] qopInChallenge, byte[] ciphersInChallenge)
395 throws IOException {
396
397
398 String qopOptions;
399
400 if (qopInChallenge == null) {
401 qopOptions = "auth";
402 } else {
403 qopOptions = new String(qopInChallenge, encoding);
404 }
405
406
407 String[] serverQopTokens = new String[3];
408 byte[] serverQop = parseQop(qopOptions, serverQopTokens,
409 true );
410 byte serverAllQop = combineMasks(serverQop);
411
412 switch (findPreferredMask(serverAllQop, qop)) {
413 case 0:
414 throw new SaslException("DIGEST-MD5: No common protection " +
415 "layer between client and server");
416
417 case NO_PROTECTION:
418 negotiatedQop = "auth";
419
420 break;
421
422 case INTEGRITY_ONLY_PROTECTION:
423 negotiatedQop = "auth-int";
424 integrity = true;
425 rawSendSize = sendMaxBufSize - 16;
426 break;
427
428 case PRIVACY_PROTECTION:
429 negotiatedQop = "auth-conf";
430 privacy = integrity = true;
431 rawSendSize = sendMaxBufSize - 26;
432 checkStrengthSupport(ciphersInChallenge);
433 break;
434 }
435
436 if (logger.isLoggable(Level.FINE)) {
437 logger.log(Level.FINE, "DIGEST61:Raw send size: {0}",
438 new Integer(rawSendSize));
439 }
440 }
441
442
443
444
445
446
447
448
449
450
451
452 private void checkStrengthSupport(byte[] ciphersInChallenge)
453 throws IOException {
454
455
456 if (ciphersInChallenge == null) {
457 throw new SaslException("DIGEST-MD5: server did not specify " +
458 "cipher to use for 'auth-conf'");
459 }
460
461
462 String cipherOptions = new String(ciphersInChallenge, encoding);
463 StringTokenizer parser = new StringTokenizer(cipherOptions, ", \t\n");
464 int tokenCount = parser.countTokens();
465 String token = null;
466 byte[] serverCiphers = { UNSET,
467 UNSET,
468 UNSET,
469 UNSET,
470 UNSET };
471 String[] serverCipherStrs = new String[serverCiphers.length];
472
473
474 for (int i = 0; i < tokenCount; i++) {
475 token = parser.nextToken();
476 for (int j = 0; j < CIPHER_TOKENS.length; j++) {
477 if (token.equals(CIPHER_TOKENS[j])) {
478 serverCiphers[j] |= CIPHER_MASKS[j];
479 serverCipherStrs[j] = token;
480 logger.log(Level.FINE, "DIGEST62:Server supports {0}", token);
481 }
482 }
483 }
484
485
486 byte[] clntCiphers = getPlatformCiphers();
487
488
489 byte inter = 0;
490 for (int i = 0; i < serverCiphers.length; i++) {
491 serverCiphers[i] &= clntCiphers[i];
492 inter |= serverCiphers[i];
493 }
494
495 if (inter == UNSET) {
496 throw new SaslException(
497 "DIGEST-MD5: Client supports none of these cipher suites: " +
498 cipherOptions);
499 }
500
501
502
503
504 negotiatedCipher = findCipherAndStrength(serverCiphers, serverCipherStrs);
505
506 if (negotiatedCipher == null) {
507 throw new SaslException("DIGEST-MD5: Unable to negotiate " +
508 "a strength level for 'auth-conf'");
509 }
510 logger.log(Level.FINE, "DIGEST63:Cipher suite: {0}", negotiatedCipher);
511 }
512
513
514
515
516
517
518
519
520
521
522 private String findCipherAndStrength(byte[] supportedCiphers,
523 String[] tokens) {
524 byte s;
525 for (int i = 0; i < strength.length; i++) {
526 if ((s=strength[i]) != 0) {
527 for (int j = 0; j < supportedCiphers.length; j++) {
528
529
530
531
532 if (s == supportedCiphers[j] &&
533 (specifiedCipher == null ||
534 specifiedCipher.equals(tokens[j]))) {
535 switch (s) {
536 case HIGH_STRENGTH:
537 negotiatedStrength = "high";
538 break;
539 case MEDIUM_STRENGTH:
540 negotiatedStrength = "medium";
541 break;
542 case LOW_STRENGTH:
543 negotiatedStrength = "low";
544 break;
545 }
546
547 return tokens[j];
548 }
549 }
550 }
551 }
552
553 return null;
554 }
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570 private byte[] generateClientResponse(byte[] charset) throws IOException {
571
572 ByteArrayOutputStream digestResp = new ByteArrayOutputStream();
573
574 if (useUTF8) {
575 digestResp.write("charset=".getBytes(encoding));
576 digestResp.write(charset);
577 digestResp.write(',');
578 }
579
580 digestResp.write(("username=\"" +
581 quotedStringValue(username) + "\",").getBytes(encoding));
582
583 if (negotiatedRealm.length() > 0) {
584 digestResp.write(("realm=\"" +
585 quotedStringValue(negotiatedRealm) + "\",").getBytes(encoding));
586 }
587
588 digestResp.write("nonce=\"".getBytes(encoding));
589 writeQuotedStringValue(digestResp, nonce);
590 digestResp.write('"');
591 digestResp.write(',');
592
593 nonceCount = getNonceCount(nonce);
594 digestResp.write(("nc=" +
595 nonceCountToHex(nonceCount) + ",").getBytes(encoding));
596
597 cnonce = generateNonce();
598 digestResp.write("cnonce=\"".getBytes(encoding));
599 writeQuotedStringValue(digestResp, cnonce);
600 digestResp.write("\",".getBytes(encoding));
601 digestResp.write(("digest-uri=\"" + digestUri + "\",").getBytes(encoding));
602
603 digestResp.write("maxbuf=".getBytes(encoding));
604 digestResp.write(String.valueOf(recvMaxBufSize).getBytes(encoding));
605 digestResp.write(',');
606
607 try {
608 digestResp.write("response=".getBytes(encoding));
609 digestResp.write(generateResponseValue("AUTHENTICATE",
610 digestUri, negotiatedQop, username,
611 negotiatedRealm, passwd, nonce, cnonce,
612 nonceCount, authzidBytes));
613 digestResp.write(',');
614 } catch (Exception e) {
615 throw new SaslException(
616 "DIGEST-MD5: Error generating response value", e);
617 }
618
619 digestResp.write(("qop=" + negotiatedQop).getBytes(encoding));
620
621 if (negotiatedCipher != null) {
622 digestResp.write((",cipher=\"" + negotiatedCipher + "\"").getBytes(encoding));
623 }
624
625 if (authzidBytes != null) {
626 digestResp.write(",authzid=\"".getBytes(encoding));
627 writeQuotedStringValue(digestResp, authzidBytes);
628 digestResp.write("\"".getBytes(encoding));
629 }
630
631 if (digestResp.size() > MAX_RESPONSE_LENGTH) {
632 throw new SaslException ("DIGEST-MD5: digest-response size too " +
633 "large. Length: " + digestResp.size());
634 }
635 return digestResp.toByteArray();
636 }
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652 private void validateResponseValue(byte[] fromServer) throws SaslException {
653 if (fromServer == null) {
654 throw new SaslException("DIGEST-MD5: Authenication failed. " +
655 "Expecting 'rspauth' authentication success message");
656 }
657
658 try {
659 byte[] expected = generateResponseValue("",
660 digestUri, negotiatedQop, username, negotiatedRealm,
661 passwd, nonce, cnonce, nonceCount, authzidBytes);
662 if (!Arrays.equals(expected, fromServer)) {
663
664 throw new SaslException(
665 "Server's rspauth value does not match what client expects");
666 }
667 } catch (NoSuchAlgorithmException e) {
668 throw new SaslException(
669 "Problem generating response value for verification", e);
670 } catch (IOException e) {
671 throw new SaslException(
672 "Problem generating response value for verification", e);
673 }
674 }
675
676
677
678
679
680
681
682
683
684 private static int getNonceCount(byte[] nonceValue) {
685 return 1;
686 }
687
688 private void clearPassword() {
689 if (passwd != null) {
690 for (int i = 0; i < passwd.length; i++) {
691 passwd[i] = 0;
692 }
693 passwd = null;
694 }
695 }
696 }